import { NodeInfo, NodeValue, State } from './types';

export class Visualizer {
  private _state: State | null = null;
  private nodes: Record<number, Node> = {};

  public setup = async (target: Window) => {
    this._state = { window: target };
  };

  public render(nodes: NodeValue[]) {
    const doc = this._state?.window.document;

    // 按照 id 排序，确保父节点先创建
    nodes.sort((a, b) => a.id - b.id);

    for (const node of nodes) {
      this.createNode(node, doc);
    }
  }

  private createNode(nodeValue: NodeValue, doc: Document) {
    const nodeInfo = nodeValue.data;
    let tag = nodeInfo.tag;
    let parent = this.element(nodeValue.parent);
    let pivot = this.element(nodeValue.previous);
    let insert = this.insertAfter;

    switch (tag) {
      case '*D':
        doc.open();
        doc.write(
          new XMLSerializer().serializeToString(
            doc.implementation.createDocumentType(
              nodeInfo.attributes?.name || 'html',
              nodeInfo.attributes?.publicId || '',
              nodeInfo.attributes?.systemId || ''
            )
          )
        );
        doc.close();
        break;
      case 'HTML':
        let docElement = this.element(nodeValue.id) as HTMLElement;
        if (docElement === null) {
          let newDoc = doc.implementation.createHTMLDocument('');
          docElement = newDoc.documentElement;
          let p = doc.importNode(docElement, true);
          doc.replaceChild(p, doc.documentElement);
          if (doc.head) {
            doc.head.parentNode.removeChild(doc.head);
          }
          if (doc.body) {
            doc.body.parentNode.removeChild(doc.body);
          }
        }
        this.setAttributes(doc.documentElement, nodeInfo);
        this.nodes[nodeValue.id] = doc.documentElement;

        break;
      case 'HEAD':
        let headElement = this.element(nodeValue.id);
        if (headElement === null) {
          headElement = doc.createElement(nodeInfo.tag);

          if (nodeInfo.attributes && '*B' in nodeInfo.attributes) {
            let base = doc.createElement('base');
            base.href = nodeInfo.attributes['*B'];
            headElement.appendChild(base);
          }
        }
        this.setAttributes(headElement as HTMLElement, nodeInfo);
        insert(nodeValue, parent, headElement, pivot);
        break;
      case 'LINK':
        let linkElement = this.element(nodeValue.id) as HTMLLinkElement;
        linkElement = linkElement ? linkElement : (doc.createElement(nodeInfo.tag) as HTMLLinkElement);
        if (!nodeInfo.attributes) {
          nodeInfo.attributes = {};
        }
        this.setAttributes(linkElement, nodeInfo);

        // todo 加载外部资源时，可以添加加载效果，让回显更友好，暂时不处理
        insert(nodeValue, parent, linkElement, pivot);
        break;
      case 'STYLE':
        let styleElement = this.element(nodeValue.id);
        styleElement = styleElement ? styleElement : doc.createElement(nodeInfo.tag);
        styleElement.nodeValue = nodeInfo.value;
        insert(nodeValue, parent, styleElement, pivot);
        break;
      case '*T':
        let textElement = this.element(nodeValue.id);
        textElement = textElement ? textElement : doc.createTextNode(null);
        textElement.nodeValue = nodeInfo.value;
        insert(nodeValue, parent, textElement, pivot);
        break;
      default:
        this.insertDefaultElement(nodeValue, parent, pivot, doc, insert);
        break;
    }
  }

  private element = (nodeId: number): Node => {
    return nodeId !== null && nodeId > 0 && nodeId in this.nodes ? this.nodes[nodeId] : null;
  };

  private insertAfter = (data: NodeValue, parent: Node, node: Node, previous: Node) => {
    let next = previous && (previous.parentElement === parent || previous.parentNode === parent) ? previous.nextSibling : null;
    next = previous === null && parent ? this.firstChild(parent) : next;

    this.insertBefore(data, parent, node, next);
  };

  private firstChild = (node: Node): ChildNode => {
    let child = node.firstChild;
    // BASE tag should always be the first child to ensure resources with relative URLs are loaded correctly
    if (child && child.nodeType === 1 && (child as HTMLElement).tagName === 'BASE') {
      return child.nextSibling;
    }
    return child;
  };

  private insertBefore = (data: NodeValue, parent: Node, node: Node, next: Node): void => {
    if (parent !== null) {
      parent.insertBefore(node, next);
    }
    this.nodes[data.id] = node;
  };

  private setAttributes = (node: HTMLElement, data: NodeInfo): void => {
    let attributes = data.attributes || {};

    for (let attribute in attributes) {
      let v = attributes[attribute];
      if (attribute.indexOf('*') === 0) {
        continue;
      }
      node.setAttribute(attribute, v);
    }
  };

  private insertDefaultElement = (
    nodeValue: NodeValue,
    parent: Node,
    pivot: Node,
    doc: Document,
    insert: (data: NodeValue, parent: Node, node: Node, previous: Node) => void
  ): void => {
    let domElement = this.element(nodeValue.id) as HTMLElement;
    domElement = domElement ? domElement : this.createElement(doc, nodeValue.data.tag);
    this.setAttributes(domElement as HTMLElement, nodeValue.data);
    insert(nodeValue, parent, domElement, pivot);
  };

  private createElement = (doc: Document, tag: string): HTMLElement => {
    return doc.createElement(tag);
  };
}
